53 Pandas模块的统计计算
53.1 引言统计计算是数据分析的核心
统计学是从数据中提取知识的科学。在金融领域,统计计算帮助我们: - 量化风险: 计算波动率、VaR(在险价值) - 评估收益: 计算收益率、夏普比率 - 发现规律: 识别相关性、趋势、周期性 - 做出决策: 基于统计显著性的投资决策
Pandas提供了丰富的统计计算方法,结合NumPy的向量化运算,可以高效处理大规模金融数据。
53.2 描述性统计
53.2.1 数学基础
给定数据集 \(X = \{x_1, x_2, \ldots, x_n\}\):
集中趋势: - 均值(Mean): \(\bar{x} = \frac{1}{n}\sum_{i=1}^n x_i\) - 中位数(Median): 排序后位于中间位置的值 - 众数(Mode): 出现频率最高的值
离散程度: - 方差(Variance): \(\sigma^2 = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})^2\) - 标准差(Standard Deviation): \(\sigma = \sqrt{\sigma^2}\) - 极差(Range): \(\max(X) - \min(X)\)
分布形状: - 偏度(Skewness): 衡量分布的对称性 - 峰度(Kurtosis): 衡量分布的尖峰/厚尾程度
53.2.2 基础统计量计算
平台任务解答代码
以下代码与教学平台任务要求完全一致:
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#任务一
import pandas as pd
import matplotlib.pyplot as plt # 导入Matplotlib绑图库
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx")
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d') # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True) # 将日期列设为value_QDII数据框的索引
value_QDII = value_QDII.dropna() #删除缺失值所在行
(value_QDII/value_QDII.iloc[0]).plot(figsize=(8,6),grid=True) #将基金净值按首个交易日进行归一处理并可视化
plt.savefig("1.png") # 保存图形至文件
#任务二
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx")
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d') # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True) # 将日期列设为value_QDII数据框的索引
print(value_QDII.max()) #找出每只基金净值的最大值
print(value_QDII.min()) #找出每只基金净值的最小值
print(value_QDII.idxmax()) #最大值所在的索引值
print(value_QDII.idxmin()) #最小值所在的索引值
#任务三
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx")
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d') # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True) # 将日期列设为value_QDII数据框的索引
value_QDII_diff = value_QDII.diff() # 计算基金每日净值的变动金额
print(value_QDII_diff.head()) #查看前五行数据
print(value_QDII_diff.tail()) #查看后五行数据
#任务四
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx")
value_QDII_pctchangel = value_QDII.pct_change() #直接使用函数pct_change计算基金每日净值百分比变动
value_QDII_pctchangel.head() # 查看value_QDII_pctchangel前5行数据
value_QDII_pctchangel.tail() # 查看value_QDII_pctchangel后5行数据
value_QDII_diff = value_QDII.diff() # 计算差分值
value_QDII_pctchange2 = value_QDII_diff/value_QDII.shift(1) #运用任务三的结果计算基金每日净值百分比变动
print(value_QDII_pctchange2.head()) # 输出前几行数据
print(value_QDII_pctchange2.tail()) # 输出最后几行数据import pandas as pd
import numpy as np
# 创建股票收益率数据
np.random.seed(42)
returns_data = {
'贵州茅台': np.random.normal(0.001, 0.02, 100), # 日收益率
'五粮液': np.random.normal(0.0008, 0.025, 100),
'招商银行': np.random.normal(0.0005, 0.015, 100),
'中国平安': np.random.normal(0.0006, 0.018, 100)
}
df_returns = pd.DataFrame(returns_data)
print('收益率数据(前10行):')
print(df_returns.head(10))
# 计算各项统计量
print('\n基础统计量:')
print(f'均值:\n{df_returns.mean()}')
print(f'\n中位数:\n{df_returns.median()}')
print(f'\n标准差:\n{df_returns.std()}')
print(f'\n方差:\n{df_returns.var()}')
print(f'\n最小值:\n{df_returns.min()}')
print(f'\n最大值:\n{df_returns.max()}')
# 一次性获取所有描述性统计
print('\n完整描述性统计:')
desc_stats = df_returns.describe()
print(desc_stats)53.2.3 describe()方法详解
describe() 方法返回的统计量:
| 统计量 | 含义 | 公式 |
|---|---|---|
| count | 非缺失值数量 | \(n_{\text{valid}}\) |
| mean | 均值 | \(\bar{x} = \frac{1}{n}\sum x_i\) |
| std | 标准差 | \(\sqrt{\frac{1}{n-1}\sum(x_i-\bar{x})^2}\) |
| min | 最小值 | \(\min(X)\) |
| 25% | 第一四分位数 | \(Q_1 = P_{25}\) |
| 50% | 第二四分位数(中位数) | \(Q_2 = P_{50}\) |
| 75% | 第三四分位数 | \(Q_3 = P_{75}\) |
| max | 最大值 | \(\max(X)\) |
53.2.4 分位数计算
# 计算常用分位数
quantiles = [0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]
print('自定义分位数:')
print(df_returns.quantile(quantiles))
# 四分位距(IQR)
Q1 = df_returns.quantile(0.25)
Q3 = df_returns.quantile(0.75)
IQR = Q3 - Q1
print('\n四分位距(IQR):')
print(IQR)
# 识别异常值(超出1.5*IQR)
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print('\n异常值边界:')
print(f'下界:\n{lower_bound}')
print(f'\n上界:\n{upper_bound}')
# 检测异常值
outliers = (df_returns < lower_bound) | (df_returns > upper_bound)
print(f'\n异常值数量:')
print(outliers.sum())异常值检测的数学原理:
箱线图规则(Boxplot Rule): - 正常值: \([Q_1 - 1.5 \times IQR, Q_3 + 1.5 \times IQR]\) - 温和异常值(Mild Outlier): 距离箱体 \(1.5-3 \times IQR\) - 极端异常值(Extreme Outlier): 距离箱体 \(> 3 \times IQR\)
Z-Score方法: \[ Z_i = \frac{x_i - \bar{x}}{\sigma} \] 通常认为 \(|Z| > 3\) 为异常值。
53.3 偏度与峰度
53.3.1 数学定义
偏度(Skewness): \[ \gamma_1 = \frac{E[(X-\mu)^3]}{\sigma^3} = \frac{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^3}{\left[\sqrt{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^2}\right]^3} \]
峰度(Kurtosis): \[ \gamma_2 = \frac{E[(X-\mu)^4]}{\sigma^4} - 3 = \frac{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^4}{\left[\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^2\right]^2} - 3 \]
53.3.2 金融意义
# 计算偏度和峰度
skewness = df_returns.skew()
kurtosis = df_returns.kurtosis()
print('偏度(Skewness):')
print(skewness)
print('\n峰度(Kurtosis):')
print(kurtosis)
# 解释
print('\n解释:')
for stock in df_returns.columns:
skew_val = skewness[stock]
kurt_val = kurtosis[stock]
# 偏度解释
if skew_val > 0.5:
skew_interp = '右偏(正偏),有较长右尾,极端正收益更多'
elif skew_val < -0.5:
skew_interp = '左偏(负偏),有较长左尾,极端负收益更多'
else:
skew_interp = '近似对称'
# 峰度解释
if kurt_val > 1:
kurt_interp = '尖峰分布,有较多极端值(厚尾)'
elif kurt_val < -1:
kurt_interp = '低峰分布,较为平坦'
else:
kurt_interp = '接近正态分布'
print(f'\n{stock}:')
print(f' 偏度={skew_val:.3f} → {skew_interp}')
print(f' 峰度={kurt_val:.3f} → {kurt_interp}')金融应用:
- 正偏度:
- 大多数时间小幅下跌
- 偶尔出现大幅上涨(如牛市中的股票)
- 符合投资者偏好(有限亏损,无限收益)
- 负偏度:
- 大多数时间小幅上涨
- 偶尔出现暴跌(如高杠杆资产)
- 风险较高(黑天鹅事件)
- 高峰度:
- 厚尾(Fat Tails): 极端事件发生的概率高于正态分布
- 金融市场的典型特征
- 风险管理需要考虑极端情况
正态分布检验:
如果数据完全服从正态分布: - 偏度 ≈ 0 - 峰度 ≈ 0(超额峰度) - Jarque-Bera检验: \(JB = \frac{n}{6}\left(\gamma_1^2 + \frac{\gamma_2^2}{4}\right)\)
53.4 累积统计量
# 创建价格序列
prices = pd.DataFrame({
'贵州茅台': [1850, 1860, 1855, 1870, 1865, 1880],
'五粮液': [220, 218, 222, 225, 223, 226]
})
print('原始价格:')
print(prices)
# 累积和
print('\n累积和:')
print(prices.cumsum())
# 累积积
print('\n累积积:')
print(prices.cumprod())
# 累积最大值
print('\n累积最大值( expanding maximum):')
print(prices.cummax())
# 累积最小值
print('\n累积最小值( expanding minimum):')
print(prices.cummin())
# 金融应用:累计收益率
initial_prices = prices.iloc[0]
cum_returns = (prices / initial_prices - 1) * 100
print('\n累计收益率(%):')
print(cum_returns)累积统计量的金融应用:
- cumsum: 累计收益、累计成交量
- cumprod: 复利增长(价格相对变化)
- cummax: 回撤分析(Drawdown)
- cummin: 历史最低价监控
53.4.1 回撤计算
# 模拟净值曲线
np.random.seed(42)
nav = pd.DataFrame({
'日期': pd.date_range('2024-01-01', periods=100),
'净值': 1.0 + np.cumsum(np.random.normal(0.001, 0.02, 100))
})
# 计算历史最高点
nav['历史最高'] = nav['净值'].cummax()
# 计算回撤
nav['回撤'] = (nav['净值'] - nav['历史最高']) / nav['历史最高']
print('净值与回撤:')
print(nav.head(20))
# 最大回撤
max_drawdown = nav['回撤'].min()
print(f'\n最大回撤: {max_drawdown:.2%}')
# 可视化
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
# 净值曲线
ax1.plot(nav['日期'], nav['净值'], label='净值', linewidth=2)
ax1.plot(nav['日期'], nav['历史最高'], label='历史最高', linewidth=2, linestyle='--')
ax1.set_ylabel('净值')
ax1.set_title('净值曲线与回撤分析')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 回撤曲线
ax2.fill_between(nav['日期'], nav['回撤'], 0, alpha=0.3, color='red')
ax2.plot(nav['日期'], nav['回撤'], color='red', linewidth=2)
ax2.set_ylabel('回撤率')
ax2.set_xlabel('日期')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()回撤的金融意义:
回撤(Drawdown):从历史最高点到当前点的下降幅度 \[ \text{Drawdown}_t = \frac{P_t - \max_{i \leq t} P_i}{\max_{i \leq t} P_i} \]
最大回撤(Maximum Drawdown, MDD): \[ \text{MDD} = \min_t \text{Drawdown}_t \]
最大回撤是衡量投资策略风险的关键指标: - MDD = -20%: 意味着历史上曾经从高点下跌20% - 心理影响: 投资者需要承受20%的亏损 - 恢复要求: 下跌20%需要上涨25%才能回本
53.5 窗口统计量滚动计算
53.5.1 数学原理
滚动窗口(Rolling Window):对于时间序列 \(\{x_t\}_{t=1}^T\),窗口大小为 \(w\):
\[ \text{RollingMean}_t = \frac{1}{w}\sum_{i=t-w+1}^t x_i \]
53.5.2 滚动统计量计算
# 创建价格数据
dates = pd.date_range('2024-01-01', periods=100)
prices_ts = pd.DataFrame({
'日期': dates,
'收盘价': 100 + np.cumsum(np.random.normal(0.5, 2, 100))
})
prices_ts = prices_ts.set_index('日期')
print('原始价格:')
print(prices_ts.tail(10))
# 5日滚动均值
prices_ts['MA5'] = prices_ts['收盘价'].rolling(window=5).mean()
# 20日滚动均值
prices_ts['MA20'] = prices_ts['收盘价'].rolling(window=20).mean()
# 5日滚动标准差
prices_ts['STD5'] = prices_ts['收盘价'].rolling(window=5).std()
# 5日滚动最大值
prices_ts['MAX5'] = prices_ts['收盘价'].rolling(window=5).max()
print('\n滚动统计量:')
print(prices_ts.tail(10))
# 计算布林带
prices_ts['布林带_上'] = prices_ts['MA20'] + 2 * prices_ts['收盘价'].rolling(window=20).std()
prices_ts['布林带_下'] = prices_ts['MA20'] - 2 * prices_ts['收盘价'].rolling(window=20).std()
print('\n布林带:')
print(prices_ts[['收盘价', 'MA20', '布林带_上', '布林带_下']].tail(10))金融技术指标:
- 移动平均(Moving Average, MA):平滑价格波动,识别趋势
- 布林带(Bollinger Bands):
- 中轨: \(MA_{20}\)
- 上轨: \(MA_{20} + 2\sigma\)
- 下轨: \(MA_{20} - 2\sigma\)
- 交易信号:价格触及上轨可能超买,触及下轨可能超卖
53.6 expanding窗口
# expanding:从起点到当前点的累积计算
prices_ts['累积均值'] = prices_ts['收盘价'].expanding().mean()
prices_ts['累积标准差'] = prices_ts['收盘价'].expanding().std()
prices_ts['累积最大值'] = prices_ts['收盘价'].expanding().max()
print('扩展窗口统计:')
print(prices_ts[['收盘价', '累积均值', '累积标准差', '累积最大值']].tail(10))
# 金融应用:累计波动率
prices_ts['累计波动率'] = prices_ts['收盘价'].pct_change().expanding().std() * np.sqrt(252)
print('\n年化累计波动率:')
print(prices_ts['累计波动率'].tail(10))rolling vs expanding:
| 特性 | rolling | expanding |
|---|---|---|
| 窗口大小 | 固定 | 不断增长 |
| 计算范围 | \([t-w+1, t]\) | \([1, t]\) |
| 权重 | 等权重 | 等权重 |
| 应用 | 移动平均、短期波动 | 累计收益、长期风险 |
53.7 分组统计 groupby
53.7.1 数学原理
分组聚合(GroupBy Aggregation): \[ \text{GroupBy}(K, f, X) = \{(k, f(\{x | key(x) = k\})) | k \in K\} \]
其中: - \(K\): 键集合 - \(f\): 聚合函数 - \(X\): 数据集
53.7.2 基础分组操作
# 创建行业数据
industry_data = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH', '000001.SZ', '601398.SH'],
'股票名称': ['贵州茅台', '五粮液', '招商银行', '中国平安', '平安银行', '工商银行'],
'行业': ['白酒', '白酒', '银行', '保险', '银行', '银行'],
'市盈率': [45.2, 35.8, 8.5, 12.3, 9.2, 6.8],
'市净率': [12.3, 8.9, 0.9, 1.5, 1.1, 0.7],
'ROE': [0.28, 0.22, 0.15, 0.18, 0.13, 0.14],
'股息率': [0.012, 0.018, 0.035, 0.028, 0.040, 0.045]
})
print('行业数据:')
print(industry_data)
# 按行业分组计算均值
industry_mean = industry_data.groupby('行业').mean(numeric_only=True)
print('\n行业均值:')
print(industry_mean)
# 按行业分组计算多个统计量
industry_stats = industry_data.groupby('行业').agg({
'市盈率': ['mean', 'median', 'std'],
'市净率': ['mean', 'min', 'max'],
'ROE': 'mean',
'股息率': 'mean'
})
print('\n行业详细统计:')
print(industry_stats.round(4))
# 分组计数
industry_count = industry_data.groupby('行业').size()
print('\n行业股票数量:')
print(industry_count)53.7.3 多级分组
# 创建多级数据
multi_level_data = pd.DataFrame({
'行业': ['白酒', '白酒', '白酒', '银行', '银行', '银行', '保险', '保险'],
'市值等级': ['大盘', '中盘', '大盘', '大盘', '小盘', '大盘', '大盘', '中盘'],
'市盈率': [45.2, 35.8, 38.5, 8.5, 15.2, 6.8, 12.3, 18.5],
'收益率': [0.05, 0.03, 0.06, 0.02, 0.04, 0.01, 0.03, 0.02]
})
df_multi = pd.DataFrame(multi_level_data)
print('多级分组统计:')
# 多级分组
multi_stats = df_multi.groupby(['行业', '市值等级']).agg({
'市盈率': 'mean',
'收益率': 'mean'
})
print(multi_stats)
# 按行业统计,并排名
df_multi['行业内PE排名'] = df_multi.groupby('行业')['市盈率'].rank()
print('\n行业内PE排名:')
print(df_multi)53.7.4 自定义聚合函数
# 定义自定义函数:计算市净率与市盈率的比率均值
def price_to_book_ratio(group):
'''计算市净率/市盈率比率'''
return (group['市净率'] / group['市盈率']).mean() # 按组计算PB/PE均值
# 定义自定义函数:基于ROE计算类夏普比率
def roe_adjusted_ratio(group, benchmark_roe=0.10):
'''计算ROE超额收益与波动的比率'''
excess_roe = group['ROE'].mean() - benchmark_roe # 超额ROE
return excess_roe / group['ROE'].std() if group['ROE'].std() > 0 else 0 # 类夏普比率
# 应用自定义函数
print('自定义聚合函数:') # 输出标题
print('\n市净率/市盈率比率:') # 输出PB/PE比率标题
print(industry_data.groupby('行业').apply(price_to_book_ratio)) # 按行业计算PB/PE比率
# 使用agg配合lambda计算市盈率范围
print('\n市盈率范围(最大-最小):') # 输出PE范围标题
print(industry_data.groupby('行业')['市盈率'].agg(lambda x: x.max() - x.min())) # 计算各行业PE极差53.8 相关性分析
53.8.1 协方差与相关系数
协方差(Covariance): \[ \text{Cov}(X, Y) = E[(X - \mu_X)(Y - \mu_Y)] = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y}) \]
相关系数(Correlation Coefficient): \[ \rho_{X,Y} = \frac{\text{Cov}(X, Y)}{\sigma_X \sigma_Y} = \frac{\sum(x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum(x_i - \bar{x})^2}\sqrt{\sum(y_i - \bar{y})^2}} \]
53.8.2 相关性计算
# 使用之前的收益率数据
df_returns_sample = df_returns
# 计算协方差矩阵
cov_matrix = df_returns_sample.cov()
print('协方差矩阵:')
print(cov_matrix)
# 计算相关系数矩阵
corr_matrix = df_returns_sample.corr()
print('\n相关系数矩阵:')
print(corr_matrix)
# 找出相关性最高的股票对
corr_unstack = corr_matrix.unstack()
corr_unstack = corr_unstack[corr_unstack != 1] # 排除自相关
top_corr = corr_unstack.abs().sort_values(ascending=False).head(5)
print('\n相关性最高的股票对:')
print(top_corr)相关性的金融意义:
| 相关系数 | 关系 | 金融含义 | 投资策略 |
|---|---|---|---|
| \(\rho \approx 1\) | 强正相关 | 同涨同跌 | 分散化效果差 |
| \(\rho \approx 0\) | 无相关 | 独立变动 | 有效分散化 |
| \(\rho \approx -1\) | 强负相关 | 此消彼长 | 对冲工具 |
现代投资组合理论(MPT): \[ \sigma_p^2 = \sum_{i=1}^n \sum_{j=1}^n w_i w_j \sigma_i \sigma_j \rho_{ij} \]
当 \(\rho_{ij} < 1\) 时,组合方差小于各资产方差的加权平均,实现风险分散。